17.4 标记
并发标记分为两个步骤。
- 扫描:遍历相关内存区域,依照指针标记找出灰色可达对象,加入队列。
- 标记:将灰色对象从队列取出,将其引用对象标记为灰色,自身标记为黑色。
扫描
扫描函数gcscan_m启动时,用户代码和MarkWorker都在运行。
mgcmark.go
func gcscan_m() { // 重置扫描标志,返回所有goroutine数量 local_allglen:=gcResetGState()
// 并发执行扫描任务,不过此处仅使用当前线程执行(避免抢占用户代码和MarkWorker资源?) // 任务单元包括所有Root和goroutine stack useOneP:=uint32(1) parforsetup(work.markfor,useOneP,uint32(_RootCount+local_allglen),false,markroot) parfordo(work.markfor) }
const( _RootData =0 _RootBss =1 _RootFinalizers =2 _RootSpans =3 _RootFlushCaches=4 _RootCount =5 )
func gcResetGState() (numgs int) { // 初始化所有goroutine相关标志 // 这些标志对于避免重复扫描很重要 for_,gp:=range allgs{ gp.gcscandone=false //set to true in gcphasework gp.gcscanvalid=false //stack has not been scanned gp.gcalloc=0 gp.gcscanwork=0 } numgs=len(allgs) return }
parfor是一个并行任务框架(详见17.7节),其功能就是将任务平分,让多个线程各领一份并发执行。为保证整个任务组能尽快完成,它允许从执行较慢的线程偷取任务。
不过扫描函数仅使用了当前线程,并未启用并发方式执行,似乎后续版本另有变化。扫描目标包括多个ROOT区域,还有全部goroutine栈。
mgcmark.go
switch i{ case_RootData: … case_RootBss: … case_RootFinalizers: … case_RootSpans: … case_RootFlushCaches: if gcphase!= _GCscan{ // 将正在被cache使用的所有span全部上交central // 将缓存在cache的stack归还给所属span.freelist flushallmcaches() }
default: //parfor按顺序为每个任务提供一个Id,所以访问allgs数组时需要去掉Root gp:=allgs[i-_RootCount]
// 收缩栈空间(此时不能执行用户代码,必须STW)
if gcphase== _GCmarktermination{
shrinkstack(gp)
}
// 调用scanstack->scanblock
//scanstack会设置和检查gcscanvalid标志,避免重复扫描
scang(gp)
}
// 将当前队列上交给全局队列 gcw.dispose() }
所有这些扫描过程,最终通过scanblock比对bitmap区域信息找出合法指针,将其目标当作灰色可达对象添加到待处理队列。
mgcmark.go
func scanblock(b0,n0 uintptr,ptrmaskuint8,gcwgcWork) { // 遍历 for i:=uintptr(0);i<n; { bits:=uint32(addb(ptrmask,i/(ptrSize8)))
// 没有标记,跳过
if bits==0{
i+=ptrSize*8
continue
}
for j:=0;j<8&&i<n;j++ {
// 有bitPointer标记
if bits&1!=0{
// 读取指针内容,目标对象地址
obj:= *(*uintptr)(unsafe.Pointer(b+i))
// 确认指针合法
if obj!=0&&arena_start<=obj&&obj<arena_used{
if obj,hbits,span:=heapBitsForObject(obj);obj!=0{
// 标记为灰色对象
greyobject(obj,b,i,hbits,span,gcw)
}
}
}
bits>>=1
i+=ptrSize
}
} }
// 将尚未标记的对象标记为灰色,并放入队列 func greyobject(obj,base,off uintptr,hbits heapBits,spanmspan,gcwgcWork) { if hbits.isMarked() { return } hbits.setMarked() gcw.put(obj) }
此处的gcWork是专门设计的高性能队列,它允许局部队列和全局队列work.full/partial协同工作,平衡任务分配(详见17.7节)。
mgc.go
var work struct{ full uint64 //lock-free list of full blocks workbuf empty uint64 //lock-free list of empty blocks workbuf partial uint64 //lock-free list of partially filled blocks workbuf }
在markroot的最后,所有扫描到的灰色对象都被提交给了work.full全局队列。
标记
并发标记由多个MarkWorker goroutine共同完成,它们在回收任务开始前被绑定到P,然后进入休眠状态,直到被调度器唤醒。
mgc.go
func gcBgMarkStartWorkers() { // 为每个P绑定一个Worker for_,p:=range&allp{ if p.gcBgMarkWorker==nil{ go gcBgMarkWorker(p)
// 暂停,确保该Worker绑定到P后再继续
notetsleepg(&work.bgMarkReady, -1)
noteclear(&work.bgMarkReady)
}
} }
调度函数schedule从控制器gcController获取MarkWorker goroutine并执行。
proc1.go
func schedule() { if gp==nil&&gcBlackenEnabled!=0{ gp=gcController.findRunnableGCWorker(g.m.p.ptr()) }
execute(gp,inheritTime) }
控制器方法findRunnableGCWorker在返回当前P所绑定的MarkWorker时,会依据当前运行状态和相关策略设置工作模式,最后还负责将其唤醒。
MarkWorker有3种工作模式。
- gcMarkWorkerDedicatedMode:全力运行,直到并发标记任务结束。
- gcMarkWorkerFractionalMode:参与标记任务,但可被抢占和调度。
- gcMarkWorkerIdleMode:仅在空闲时参与标记任务。
在了解基本运作流程后,我们去看看标记工作的具体内容。
mgc.go
func gcBgMarkWorker(p*p) { // 将当前goroutine绑定到P gp:=getg() p.gcBgMarkWorker=gp
// 唤醒外层创建循环 notewakeup(&work.bgMarkReady)
for{ // 休眠,直到被gcContoller.findRunnable唤醒 gopark(…, “mark worker(idle)”, …,0)
// 只能在进入黑化阶段才能运行
if gcBlackenEnabled==0{
throw("gcBgMarkWorker:blackening not enabled")
}
decnwait:=xadd(&work.nwait, -1)
done:=false
// 工作模式
switch p.gcMarkWorkerMode{
case gcMarkWorkerDedicatedMode:
// 全力工作,直到全部任务结束
gcDrain(&p.gcw,gcBgCreditSlack)
done=true
if!p.gcw.empty() {
throw("gcDrain returned with buffer")
}
case gcMarkWorkerFractionalMode,gcMarkWorkerIdleMode:
// 在抢占或无法获取任务时退出
gcDrainUntilPreempt(&p.gcw,gcBgCreditSlack)
// 立即上交剩余缓存队列
if gcBlackenPromptly{
p.gcw.dispose()
}
incnwait:=xadd(&work.nwait, +1)
done=incnwait==work.nproc&&work.full==0&&work.partial==0
}
// 如果标记任务全部完成,则发送信号
if done{
// 该标志在截获bgMark1后才被设置,确保bgMark2在bgMark1之后发送
if gcBlackenPromptly{
if work.bgMark1.done==0{
throw("completing mark 2,but bgMark1.done==0")
}
work.bgMark2.complete()
}else{
work.bgMark1.complete()
}
}
} }
不同模式的MarkWorker对待工作的态度完全不同。
mgcmark.go
func gcDrain(gcw*gcWork,flushScanCredit int64) { for{ // 如果全局队列已空,且有等待的Worker,那么分出一部分任务 if work.nwait>0&&work.full==0{ gcw.balance() }
// 反复尝试从本地或全局队列获取任务,直到所有Worker完成任务
b:=gcw.get()
if b==0{
break
}
scanobject(b,gcw)
} }
func gcDrainUntilPreempt(gcw*gcWork,flushScanCredit int64) { gp:=getg()
// 检查抢占标志 for!gp.preempt{ // 只要全局队列为空,就立即分出一部分任务,不关心是否有Worker进入等待状态 if work.full0&&work.partial0{ gcw.balance() }
// 尝试从本地或全局获取任务,失败则放弃。不关心其他Worker是否完成任务
b:=gcw.tryGet()
if b==0{
break
}
scanobject(b,gcw)
} }
处理灰色对象时,无须知道其真实大小,只当作内存分配器提供的object块即可。按指针类型长度对齐,配合bitmap标记进行遍历,就可找出所有引用成员,将其作为灰色对象压入队列。当然,当前对象自然成为黑色对象,从队列移除。
mgcmark.go
func scanobject(b uintptr,gcw*gcWork) { hbits:=heapBitsForAddr(b) s:=spanOfUnchecked(b) n:=s.elemsize
for i=0;i<n;i+=ptrSize{ bits:=hbits.bits()
// 标记位检查
if i>=2*ptrSize&&bits&bitMarked==0{
break //no more pointers in this object
}
if bits&bitPointer==0{
continue //not a pointer
}
// 读取指针内容,成员所引用对象地址
obj:= *(*uintptr)(unsafe.Pointer(b+i))
// 确认指针合法
if obj!=0&&arena_start<=obj&&obj<arena_used&&obj-b>=n{
// 将引用对象标记为灰色
if obj,hbits,span:=heapBitsForObject(obj);obj!=0{
greyobject(obj,b,i,hbits,span,gcw)
}
}
} }
在STW启动后,承担最终收尾工作的gcMark有点特殊。如果并发标记被禁用,那么它就需要完成全部的标记任务,回退到Go 1.4的阻塞工作模式。
mgc.go
func gcMark(start_time int64) { // 确保所有任务都上交到全局队列 gcFlushGCWork()
work.nproc=uint32(gcprocs())
// 并发执行扫描任务(这次不是单个线程了) // 因为已经STW,所以这次需要做flushallmcaches、shrinkstack操作 parforsetup(work.markfor,work.nproc,uint32(_RootCount+allglen),false,markroot) if work.nproc>1{ // 重置休眠标志 noteclear(&work.alldone)
//parfor并发执行的关键
helpgc(int32(work.nproc))
}
// 当前线程一起参加mark+drain任务 gchelperstart() parfordo(work.markfor) var gcw gcWork gcDrain(&gcw, -1) gcw.dispose()
// 休眠,等待gchelper任务结束后被唤醒 if work.nproc>1{ notesleep(&work.alldone) }
// 释放不再使用的stack缓存对象 freeStackSpans()
// 更新cache状态(被markroot处理过) cachestats()
// 计算下次回收阈值 memstats.next_gc= …(memstats.heap_reachable) * (1+gcController.triggerRatio))
// 不能小于最低阈值4 MB if memstats.next_gc<heapminimum{ memstats.next_gc=heapminimum }
minNextGC:=memstats.heap_live+sweepMinHeapDistance*uint64(gcpercent)/100 if memstats.next_gc<minNextGC{ memstats.next_gc=minNextGC } }
因为有gcController决策算法的参与,垃圾回收阈值next_gc变得更加灵活。
相比gcscan_m+MarkWorker,gcMark显然简单得多,关键问题就是gchelper如何执行。
1.函数helpgc唤醒足够数量的线程M用于执行parfordo任务。
2.被唤醒的M检查helpgc标志,执行gchelper函数完成mark+drain任务。
有关M执行方式,请参考本书后续“并发调度”相关内容。
proc1.go
func helpgc(nproc int32) { pos:=0
// 从1开始,因为当前线程(M)也参加并发任务 for n:=int32(1);n<nproc;n++ { // 跳过当前M正在使用的P if allp[pos].mcache== g.m.mcache{ pos++ }
// 获取并设置M参数
mp:=mget()
mp.helpgc=n // 关键
mp.p.set(allp[pos])
mp.mcache=allp[pos].mcache
pos++
// 唤醒M去执行任务
notewakeup(&mp.park)
} }
proc1.go
func stopm() { g :=getg()
retry: mput(g.m)
// 休眠 notesleep(&g.m.park) noteclear(&g.m.park)
// 被唤醒后,检查helpgc标志 if_g_.m.helpgc!=0{ // 执行gchelper函数 gchelper() } }
mgc.go
func gchelper() { g :=getg() gchelperstart()
// 执行mark+drain任务 parfordo(work.markfor) if gcphase!= _GCscan{ var gcw gcWork gcDrain(&gcw, -1) //blocks in getfull gcw.dispose() }
nproc:=work.nproc
// 如果全部任务(注意 -1)完成,那么唤醒GC线程 if xadd(&work.ndone, +1) ==nproc-1{ notewakeup(&work.alldone) } }